package online.yangcloud.web.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.io.FileUtil;
import cn.hutool.core.util.ObjectUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.crypto.SecureUtil;
import cn.hutool.json.JSONUtil;
import online.yangcloud.common.annotation.TimeConsuming;
import online.yangcloud.common.common.AppConstants;
import online.yangcloud.common.enumration.FileTypeEnum;
import online.yangcloud.common.enumration.YesOrNoEnum;
import online.yangcloud.common.tools.FileTools;
import online.yangcloud.common.tools.GlobalTools;
import online.yangcloud.common.tools.IdTools;
import online.yangcloud.common.tools.SystemTools;
import online.yangcloud.core.common.StaticConstants;
import online.yangcloud.core.model.entity.*;
import online.yangcloud.core.model.request.file.FileMerger;
import online.yangcloud.core.model.request.file.FileUploadInstance;
import online.yangcloud.core.model.request.file.FileUploadParameter;
import online.yangcloud.core.model.request.file.HashChecker;
import online.yangcloud.core.model.view.file.FileMetadataView;
import online.yangcloud.web.service.UploadService;
import online.yangcloud.web.service.meta.impl.MetaServiceImpl;
import org.redisson.api.RLock;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import javax.annotation.Resource;
import java.io.IOException;
import java.nio.file.Files;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

/**
 * @author zhuboyang
 * @since 2023年11月28 10:59:55
 */
@Service
@Transactional(rollbackFor = Exception.class)
@TimeConsuming
public class UploadServiceImpl implements UploadService {

    private static final Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);

    @Resource
    private MetaServiceImpl metaService;

    @Resource
    private GlobalTools globalTools;

    @Override
    public List<Boolean> checkBlockExist(HashChecker checker) {
        // 查询所有相关的文件块
        List<String> blocks = StrUtil.split(checker.getHashList(), StrUtil.COMMA);
        List<BlockEntity> metadata = metaService.acquireBlock().queryListByHashList(blocks);

        // 将 hash 列表记录缓存
        globalTools.acquireRedis().set(
                StaticConstants.Uploader.HASH_CHECK + checker.getIdentifier(),
                JSONUtil.toJsonStr(blocks)
        );

        // 检测文件块是否都已存在
        List<String> hashList = metadata.stream().map(BlockEntity::getHash).collect(Collectors.toList());

        // 封装结果信息并返回
        return blocks.stream().map(hashList::contains).collect(Collectors.toList());
    }

    @Override
    public Integer blockUpload(FileUploadInstance instance) {
        // 检测当前文件块是否已存到硬盘上。如果没有，则需要存到磁盘上，并入库
        FileUploadParameter parameter = JSONUtil.toBean(instance.getParameter(), FileUploadParameter.class);
        RLock blockUploadLocker = globalTools.acquireRedisson()
                .getLock(StaticConstants.Uploader.BLOCK_UPLOAD_CONCURRENCE_LOCK + parameter.getIdentifier());
        try {
            blockUploadLocker.lock();
            BlockEntity block = metaService.acquireBlock().queryByHash(parameter.getHash());

            // 如果检测到文件块还不存在，就说明还没有上传过，那么先进行落地
            if (ObjectUtil.isNull(block)) {
                String uploadPath = SystemTools.systemPath() + StaticConstants.Uploader.BLOCK;
                if (!FileUtil.exist(uploadPath)) {
                    FileUtil.mkdir(uploadPath);
                }
                instance.getFile().transferTo(FileUtil.newFile(uploadPath + parameter.getHash()));
            }
            block = BlockEntity.builder()
                    .setId(IdTools.fastSimpleUuid())
                    .setHash(parameter.getHash())
                    .setPath(StaticConstants.Uploader.BLOCK + parameter.getHash())
                    .setSize(parameter.getCurrentSize());
            metaService.acquireBlock().insertBlock(block);
        } catch (IOException e) {
            logger.error(e.getMessage(), e);
        } finally {
            blockUploadLocker.unlock();
        }
        return YesOrNoEnum.YES.code();
    }

    @Override
    public FileMetadataView fileMerge(FileMerger merger, UserEntity user) throws IOException {
        // 获取完整的 hash 列表
        String cache = globalTools.acquireRedis().get(StaticConstants.Uploader.HASH_CHECK + merger.getIdentifier());
        globalTools.acquireRedis().delete(StaticConstants.Uploader.HASH_CHECK + merger.getIdentifier());
        List<String> hashList = JSONUtil.toList(cache, String.class);
        List<BlockEntity> blocks = metaService.acquireBlock().queryListByHashList(hashList);

        // 对文件块列表进行排序
        Map<String, BlockEntity> blockMap = blocks.stream().collect(Collectors.toMap(BlockEntity::getHash, o -> o));
        blocks.clear();
        for (String hash : hashList) {
            blocks.add(blockMap.get(hash));
        }

        // 生成文件 id，并生成关联数据
        String fileId = IdTools.fastSimpleUuid();
        List<FileBlockEntity> associations = generateFileBlocks(fileId, merger, blocks);
        metaService.acquireFileBlock().batchInsert(associations);

        // 计算文件的 hash
        String hash = generateFileHash(fileId, merger.getExt(), blocks);

        // 计算文件名称
        String name = calculateFileName(merger);
        FileEntity parent = metaService.acquireFile().queryById(merger.getPid(), user.getId());
        List<String> ancestors = parent.queryAncestors();
        ancestors.add(parent.getId());
        FileEntity file = FileEntity.builder()
                .setId(fileId)
                .setPid(merger.getPid())
                .setName(name)
                .setHash(hash)
                .setExt(merger.getExt())
                .setPath(StaticConstants.Uploader.FILE + hash)
                .setType(FileTypeEnum.FILE.code())
                .setSize(merger.getFileSize())
                .setterAncestors(ancestors)
                .setUploadTime(DateUtil.date().getTime())
                .setUserId(user.getId());
        metaService.acquireFile().insertWidthPrimaryKey(file);

        // 记录各类型文件的详细元数据
        metaService.acquireSnapshotConvert().addPreviewTask(PreviewConvertEntity.builder()
                .setId(IdTools.fastSimpleUuid())
                .setFileId(fileId)
                .setIsOver(YesOrNoEnum.NO.code()));

        // 开启文件预览转换任务
        globalTools.acquireRedis().set(StaticConstants.PreviewConverter.CONVERT_TASK, fileId);

        // 增加空间使用量
        globalTools.acquireRedis().set(AppConstants.Account.INCREMENT + user.getId() + StrUtil.COLON + file.getSize(), StrUtil.EMPTY);
        globalTools.acquireRedis().set(StaticConstants.PreviewConverter.CONVERT_LOCK + fileId, fileId);

        // 清空文件块的缓存记录
        globalTools.acquireRedis().delete(StaticConstants.Uploader.FILE_BLOCK_UPLOAD_PREFIX + merger.getIdentifier());
        return BeanUtil.copyProperties(file, FileMetadataView.class);
    }

    /**
     * 生成关联关系数据
     *
     * @param fileId 文件 id
     * @param merger 请求参数
     * @param blocks 文件块数据
     * @return 关联关系数据
     */
    private List<FileBlockEntity> generateFileBlocks(String fileId, FileMerger merger, List<BlockEntity> blocks) {
        List<FileBlockEntity> associations = new ArrayList<>();
        for (int i = 0; i < blocks.size(); i++) {
            BlockEntity metadata = blocks.get(i);
            associations.add(FileBlockEntity.builder()
                    .setId(IdTools.fastSimpleUuid())
                    .setFileId(fileId)
                    .setBlockId(metadata.getId())
                    .setIndex(i)
                    .setCount(blocks.size())
                    .setShardingSize(merger.getShardingSize())
                    .setFileSize(merger.getFileSize())
                    .setIsShard(YesOrNoEnum.YES.code()));
        }
        return associations;
    }

    /**
     * 生成文件 hash
     *
     * @param fileId 文件 id
     * @param ext    文件后缀
     * @param blocks 文件块数据
     * @return 文件 hash
     * @throws IOException IOException
     */
    private String generateFileHash(String fileId, String ext, List<BlockEntity> blocks) throws IOException {
        List<String> paths = blocks.stream().map(o -> SystemTools.systemPath() + o.getPath()).collect(Collectors.toList());
        String filePath = SystemTools.systemPath() + StaticConstants.Uploader.FILE + fileId + ext;
        FileTools.combineFile(filePath, paths);
        return SecureUtil.md5(Files.newInputStream(FileUtil.file(filePath).toPath()));
    }

    /**
     * 计算文件入库的文件名（如果有名称重复的，需要加序号）
     *
     * @param merger 请求参数
     * @return 文件名
     */
    private String calculateFileName(FileMerger merger) {
        int fileNumber = FileEntity.calculateFileSuffixNumber(metaService.acquireFile().queryLikePrefix(
                merger.getPid(),
                merger.getFileName(),
                FileTypeEnum.FILE
        ), merger.getFileName());
        String name = merger.getFileName();
        return fileNumber == 0 ? name : name + AppConstants.Special.LEFT_BRACKET + fileNumber + AppConstants.Special.RIGHT_BRACKET;
    }

}
